package gin_chunk

import (
	"crypto/md5"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
)

const (
	tmpDir  string = "./temp"
	fileDir string = "./uploads"
)

type Engine struct {
	*gin.Engine
	routers Routers
}

type Router interface {
	Execute(c *gin.Engine)
}

type Routers []Router

// New 入口
func New(eng *gin.Engine) *Engine {
	return &Engine{
		Engine:  eng,
		routers: make(Routers, 0),
	}
}

// Register 注册路由
func (c *Engine) Register(routers ...Router) {
	c.routers = append(c.routers, routers...)
}

// Prepare execute this method to register a route before starting running
func (c *Engine) Prepare() {
	for _, router := range c.routers {
		router.Execute(c.Engine)
	}
}

type ChunkFileRequest struct {
	FileId     string `json:"fileId"`   // 客户端传过来的uuid
	FileName   string `json:"fileName"` // file name
	ChunkRange string `json:"chunkRange"`
	ChunkSize  int    `json:"chunkSize"` // 块大小
	ChunkHash  string `json:"chunkHash"` // file now key to md5Check - if server read the slice to md5Check eq key not eq then fail
	FileHash   string `json:"fileHash"`  // file now key to md5Check - if server read the slice to md5Check eq key not eq then fail
	Data       []byte `json:"data"`      // now file
}

type ChunkMergeRequest struct {
	UUID       string `json:"uuid"`     // 客户端传过来的uuid
	FileName   string `json:"fileName"` // file name
	FileHash   string `json:"fileHash"` // file now key to md5Check - if server read the slice to md5Check eq key not eq then fail
	ChunkSize  int64  `json:"chunkSize"`
	ChunkTotal int    `json:"chunkTotal"`
}

func (cf *ChunkFileRequest) BindingForm(ctx *gin.Context) error {
	if err := ctx.ShouldBind(cf); err != nil {
		return err
	}
	if !cf.md5Check() {
		return errors.New("md5 check error")
	}
	return nil
}

func (cf *ChunkFileRequest) md5Check() bool {
	hash := fmt.Sprintf("%x", md5.Sum(cf.Data))
	return hash == cf.ChunkHash
}

func (cf *ChunkFileRequest) save2tempFile(tempPath string) error {
	tempFolder := filepath.Join(tempPath, cf.FileId)
	err := mkdirIfNotExist(tempFolder)
	if err != nil {
		return err
	}

	// 分块数据写入子目录 tmp/uuid/range-total
	out, err := os.Create(filepath.Join(tempFolder, cf.ChunkRange))
	if err != nil {
		return err
	}
	defer out.Close()
	if _, err := out.Write(cf.Data); err != nil {
		return err
	}
	return nil
}

// 直接将文件写入目标位置 不需要合并分片文件
func (cf *ChunkFileRequest) save2uploadFile(uploadDir string) error {
	var err error
	err = mkdirIfNotExist(uploadDir)
	if err != nil {
		return err
	}

	// 1. 创建目标文件
	dest, err := os.OpenFile(filepath.Join(uploadDir, cf.FileName), os.O_RDWR|os.O_CREATE, 0664)
	if err != nil {
		return err
	}
	defer dest.Close()

	// 2.校验md5 在bind时已检验

	// 3. 将分块数据写入文件指定位置
	sps := strings.Split(cf.ChunkRange, "-")
	if len(sps) == 2 {
		index, _ := strconv.Atoi(sps[0])
		dest.WriteAt(cf.Data, int64(index*cf.ChunkSize))
	} else {
		return errors.New("range 格式有误 应该为 index-total")
	}
	return nil
}

func ChunkUploadFile(ctx *gin.Context) {
	var cf ChunkFileRequest

	if err := cf.BindingForm(ctx); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"code": "400", "msg": "bad file param", "err": err.Error()})
		return
	}

	//err := cf.save2tempFile(tmpDir)
	err := cf.save2uploadFile(fileDir)
	if err != nil {
		ctx.JSON(http.StatusServiceUnavailable, gin.H{"code": "503", "msg": "bad save upload file", "err": err.Error()})
		return
	}
	ctx.JSON(http.StatusOK, gin.H{"code": "200", "msg": "success"})
}

func ChunkMerge(ctx *gin.Context) {
	var mergeRequest = ChunkMergeRequest{}
	err := ctx.ShouldBind(&mergeRequest)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"code": "400", "msg": "bad merge param", "err": err.Error()})
		return
	}

	filePath := filepath.Join(fileDir, mergeRequest.FileName)
	// |os.O_APPEND
	targetFile, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0664)
	defer targetFile.Close()

	hashMd5 := md5.New()
	for i := 0; i < mergeRequest.ChunkTotal; i++ {
		rangeFilePath := fmt.Sprintf("%d-%d", i, mergeRequest.ChunkTotal-1)
		chunkData, _ := os.ReadFile(filepath.Join(tmpDir, mergeRequest.UUID, rangeFilePath))
		targetFile.WriteAt(chunkData, int64(i)*mergeRequest.ChunkSize)
		//targetFile.Write(chunkData)
		hashMd5.Write(chunkData)
	}
	targetFileHash := fmt.Sprintf("%x", hashMd5.Sum(nil))
	if targetFileHash != mergeRequest.FileHash {
		ctx.JSON(http.StatusBadRequest, gin.H{"code": "400", "msg": "merge File failed"})
		return
	}
	_ = os.RemoveAll(filepath.Join(tmpDir, mergeRequest.UUID))
	ctx.JSON(http.StatusOK, gin.H{"code": "200", "msg": "success"})
}

func mkdirIfNotExist(path string) error {
	var err error
	_, err = os.Stat(path)
	if os.IsNotExist(err) {
		err = os.MkdirAll(path, os.ModePerm)
		if err != nil {
			return err
		}
	}
	return nil
}
